package sg.edu.nus.iss.billing.controller;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import sg.edu.nus.iss.billing.BillingConstants;
import sg.edu.nus.iss.billing.controller.ChargeController.Charges;
import sg.edu.nus.iss.billing.dao.BaseDAO;
import sg.edu.nus.iss.billing.dao.DaoFactory;
import sg.edu.nus.iss.billing.dao.RateDAO;
import sg.edu.nus.iss.billing.model.account.Account;
import sg.edu.nus.iss.billing.model.bill.Bill;
import sg.edu.nus.iss.billing.model.bill.BillGeneratedHistory;
import sg.edu.nus.iss.billing.model.bill.BillMonth;
import sg.edu.nus.iss.billing.model.bill.BillReport;
import sg.edu.nus.iss.billing.model.bill.BillingPeriod;
import sg.edu.nus.iss.billing.model.calculation.PerDay;
import sg.edu.nus.iss.billing.model.charge.IChargable;
import sg.edu.nus.iss.billing.model.company.Company;
import sg.edu.nus.iss.billing.model.subscription.CableSubscription;
import sg.edu.nus.iss.billing.model.subscription.DigitalSubscription;
import sg.edu.nus.iss.billing.model.subscription.ISubscribable;
import sg.edu.nus.iss.billing.model.subscription.ISubscription;
import sg.edu.nus.iss.billing.model.subscription.MobileSubscription;
import sg.edu.nus.iss.billing.model.subscription.Subscription;
import sg.edu.nus.iss.billing.model.transaction.PaymentTransaction;
import sg.edu.nus.iss.billing.utils.DateHelper;
import sg.edu.nus.iss.billing.utils.FileUtils;

/**
 * @author ZiFeng, Thurein, YinNi
 */
public class BillController {

	public String generateBill(String period) {
		String msg = null;
		String nextPeriod = getNextGeneratedPeriod();
		if (nextPeriod == null || period.equals(nextPeriod)) {
			BillGeneratedHistory history = getBillGeneratedHistory(nextPeriod);
			if (history == null) {
				TransactionController controller = new TransactionController();
				String transactionDataLocation = "data/inbound/transactions_" + period + ".txt";
				String paymentDataLocation = "data/inbound/payment_" + period + ".txt";
				try {
					if (!FileUtils.fileExists(transactionDataLocation) || !FileUtils.fileExists(paymentDataLocation))
						return "Bill will not be generated for the following reasons:\n - Transaction file or payment file doesn't exist for "
								+ covertorToDateDisplay(period);
				} catch (Exception e) {
					return "Bill will not be generated for the following reasons:\n - Transaction file or payment file doesn't exist for "
							+ covertorToDateDisplay(period);
				}

				Map<String, PaymentTransaction> paymentMap = controller.getPaymentTransaction(paymentDataLocation);

				List<Account> accountList = getAccountList();
				for (Account account : accountList) {
					Bill bill = account.generateBill(new BillingPeriod(getBillMonth(period.substring(4, 6)), Integer
							.parseInt(period.substring(0, 4))));
					BillReport report = BillController.mapToBillReport(bill, new BillReport());
					setBillReport(account, report, paymentMap.get(account.getAccountNo()), period);
					account.getBillMap().put(period, report);
					saveAccount(account);
				}
				history = new BillGeneratedHistory(period, transactionDataLocation);
				saveBillGeneratedHistory(history);
				printBills(period);
				msg = "Bill Generated Successfully!";
			} else {
				msg = "Bill For This Month Exsits Already!";
			}
		} else {
			msg = "Bill must be generated with correct month in sequence.";
		}
		return msg;
	}

	private void setBillReport(Account account, BillReport report, PaymentTransaction payment, String period) {

		report.setPreviousBalance(getPreviousBalance(account));

		report.setBillDate(getBillDate(period));
		report.setDueDate(getDueDate(period));
		List<Subscription> subscriptionList = account.getSubscriptions();
		if (subscriptionList != null) {
			for (Subscription subscription : subscriptionList) {
				if (subscription instanceof DigitalSubscription) {
					DigitalSubscription digital = (DigitalSubscription) subscription;
					report.setDigitalNum(String.valueOf(digital.getTelephoneNumber()));
				} else if (subscription instanceof MobileSubscription) {
					MobileSubscription mobile = (MobileSubscription) subscription;
					report.setMobileNum(String.valueOf(mobile.getTelephoneNumber()));
				} else if (subscription instanceof CableSubscription) {
					double rate = RateDAO.getInstance().getAdditionalChSubscriptionChargeRate();
					report.setCableNum(rate + " x " + String.valueOf(report.getCableTVUsageCharges() / rate));
				}
			}
		}
		if (payment != null) {
			account.getPaymentMap().put(period, payment);
			report.setPayment(payment);
		} else {
			report.setPayment(getLastestInfoByMapKey(account.getPaymentMap()));
		}

	}

	private double getPreviousBalance(Account account) {
		double previousBalance = 0;
		previousBalance = totalBalance(account.getBillMap().values()) - totalPayment(account.getPaymentMap().values());
		return previousBalance;
	}

	private double totalBalance(Collection<BillReport> collection) {
		double balance = 0;
		if (collection != null) {
			for (BillReport bill : collection) {
				balance += bill.getTotalCurrentCharges();
			}
		}
		return balance;
	}

	private double totalPayment(Collection<PaymentTransaction> collection) {
		double payment = 0;
		if (collection != null) {
			for (PaymentTransaction paymentTran : collection) {
				payment += paymentTran.getPaymentAmt();
			}
		}
		return payment;
	}

	private <T> T getLastestInfoByMapKey(Map<String, T> map) {
		if (map.keySet() != null && map.keySet().size() > 0) {
			Object[] dateStr = map.keySet().toArray();
			java.util.Arrays.sort(dateStr);
			return map.get(dateStr[dateStr.length - 1]);
		} else {
			return null;
		}
	}

	public String viewBill(String accountNo, String period) {
		Account account = getAccount(accountNo);
		BillReport bill = account.getBillMap().get(period);
		// PaymentTransaction payment = account.getPaymentMap().get(period);
		if (bill != null) {
			return FileUtils.generateTemplateString(BillingConstants.BILL_REPORT_TEMPLATE, null);
		} else
			return null;

	}

	public String viewBill(Account account, String period) {
		Map<String, Object> reportMap = new HashMap<String, Object>();
		reportMap.putAll(new Company().getMap());
		reportMap.putAll(account.getMap());
		BillReport bill = account.getBillMap().get(period);

		if (bill != null) {
			PaymentTransaction payment = bill.getPayment();
			double paymentAmount = 0;

			if (payment != null) {
				reportMap.putAll(payment.getMap());
				paymentAmount = payment.getPaymentAmt();
			}

			bill.setCurrentBalance(bill.getPreviousBalance() - paymentAmount + bill.getTotalCurrentCharges());
			reportMap.putAll(bill.getMap());
			return FileUtils.generateTemplateString(BillingConstants.BILL_REPORT_TEMPLATE, reportMap);
		} else {
			return null;
		}
	}

	private void printBills(String period) {
		try {
			FileWriter fstream = new FileWriter("data/outbound/bills_" + period + ".html");
			BufferedWriter out = new BufferedWriter(fstream);

			out.write("<html><body>");

			List<Account> accountList = getAccountList();
			for (Account account : accountList) {
				String billString = viewBill(account, period);
				if (billString != null && !billString.equals("")) {
					billString = billString.replaceAll("<html>", "");
					billString = billString.replaceAll("<body>", "");
					billString = billString.replaceAll("</body>", "");
					billString = billString.replaceAll("</html>", "");
					out.write("<br><hr><br>");
					out.write(billString);
				}
			}

			out.write("<br><hr><br></body></html>");

			out.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public Map<String, String> getViewBillMap(String accountNo) {
		Account account = getAccount(accountNo);
		if (account != null) {
			Set<String> period = account.getBillMap().keySet();
			return formatPeriod(period);
		}
		return null;
	}

	public String getNextGeneratedPeriod() {
		String correctPeriod = null;
		BaseDAO<BillGeneratedHistory> billHistoryDao = DaoFactory
				.getBillGeneratedHistoryDAO(BillingConstants.DATASOURCE_BILLGENERATEDHISTORY);
		List<BillGeneratedHistory> list = billHistoryDao.getElementList();
		List<String> periodList = new ArrayList<String>();
		if (list != null && list.size() > 0) {
			for (BillGeneratedHistory history : list)
				periodList.add(history.getPeriod());
			String[] strs = periodList.toArray(new String[0]);
			java.util.Arrays.sort(strs);
			correctPeriod = getNextMonth(strs[strs.length - 1]);
		}
		return correctPeriod;
	}

	public BillGeneratedHistory getBillGeneratedHistory(String period) {
		BillGeneratedHistory billHistory = null;
		BaseDAO<BillGeneratedHistory> billHistoryDao = DaoFactory
				.getBillGeneratedHistoryDAO(BillingConstants.DATASOURCE_BILLGENERATEDHISTORY);
		List<BillGeneratedHistory> list = billHistoryDao.getElementListByAttribute("period", period);
		if (list != null && list.size() > 0) {
			billHistory = list.get(0);
		}
		return billHistory;
	}

	public BillGeneratedHistory saveBillGeneratedHistory(BillGeneratedHistory bill) {
		BaseDAO<BillGeneratedHistory> billHistoryDao = DaoFactory
				.getBillGeneratedHistoryDAO(BillingConstants.DATASOURCE_BILLGENERATEDHISTORY);
		return billHistoryDao.saveElementByPK(bill, "period", bill.getPeriod());
	}

	public Account getAccount(String accountNo) {
		Account account = null;
		BaseDAO<Account> accountDao = DaoFactory.getAccountDAO(BillingConstants.DATASOURCE_ACCOUNT);
		List<Account> list = accountDao.getElementListByAttribute("accountNo", accountNo);
		if (list != null && list.size() > 0) {
			account = list.get(0);
		}
		return account;
	}

	public List<Account> getAccountList() {
		BaseDAO<Account> accountDao = DaoFactory.getAccountDAO(BillingConstants.DATASOURCE_ACCOUNT);
		List<Account> list = accountDao.getElementList();
		return list;
	}

	public Account saveAccount(Account account) {
		BaseDAO<Account> accountDao = DaoFactory.getAccountDAO(BillingConstants.DATASOURCE_ACCOUNT);
		return accountDao.saveElementByPK(account, "accountNo", account.getAccountNo());
	}

	private Map<String, String> formatPeriod(Set<String> period) {
		Map<String, String> map = new HashMap<String, String>();
		for (String date : period) {
			map.put(date, covertorToDateDisplay(date));
		}
		return map;
	}

	public static String covertorToDateDisplay(String period) {
		SimpleDateFormat ff = new SimpleDateFormat("yyyyMM");
		Date date;
		try {
			date = ff.parse(period);
		} catch (ParseException e) {
			return "0000";
		}
		SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("MMM yyyy");
		String str = localSimpleDateFormat.format(date);
		return str;
	}

	public static String covertorToDateString(String period) {
		SimpleDateFormat ff = new SimpleDateFormat("MMM yyyy");
		Date date;
		try {
			date = ff.parse(period);
		} catch (ParseException e) {
			return "0000";
		}
		SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("yyyyMM");
		String str = localSimpleDateFormat.format(date);
		return str;
	}

	public static String getDueDate(String period) {
		Date date = DateHelper.lastDateofMonth(Integer.parseInt(period.substring(0, 4)),
				Integer.parseInt(period.substring(4, 6)) - 1, 1);
		Date dueDate = DateHelper.addDates(date, 20);
		SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("dd-MMM-yyyy");
		String str = localSimpleDateFormat.format(dueDate);
		return str;
	}

	public static String getBillDate(String period) {
		Date date = DateHelper.lastDateofMonth(Integer.parseInt(period.substring(0, 4)),
				Integer.parseInt(period.substring(4, 6)) - 1, 1);

		SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("dd-MMM-yyyy");
		String str = localSimpleDateFormat.format(date);
		return str;
	}

	public static String getNextMonth(String period) {
		Date date = DateHelper.lastDateofMonth(Integer.parseInt(period.substring(0, 4)),
				Integer.parseInt(period.substring(4, 6)) - 1, 1);

		Date nextDate = DateHelper.addDates(date, 1);
		SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("yyyyMM");
		String str = localSimpleDateFormat.format(nextDate);
		return str;
	}

	public static BillReport mapToBillReport(Bill bill, BillReport report) {
		Map<ISubscription, List<IChargable>> subscriptionCharges = bill.getCharges();

		for (ISubscription subscription : subscriptionCharges.keySet()) {
			if (subscription instanceof MobileSubscription) {
				MobileSubscription mobile = (MobileSubscription) subscription;
				report = mapMobileCharges(subscriptionCharges.get(mobile), bill, report);
			}

			if (subscription instanceof DigitalSubscription) {
				DigitalSubscription digitalVoice = (DigitalSubscription) subscription;
				report = mapDigitalLineCharges(subscriptionCharges.get(digitalVoice), bill, report);
			}

			if (subscription instanceof CableSubscription) {
				CableSubscription cable = (CableSubscription) subscription;
				report = mapCableCharges(subscriptionCharges.get(cable), bill, report);
			}
		}
		return report;
	}

	private static BillReport mapMobileCharges(List<IChargable> charges, Bill bill, BillReport report) {

		IChargable mobileDataServicesCharge = getChargeByDesc(charges,
				Charges.MobileDataServiceSubscriptionCharge.toString());
		if (mobileDataServicesCharge != null) {
			report.setMobileDataServices(mobileDataServicesCharge.charge(bill));
			long NoOfChargingDays = PerDay.getNoOfChargingDays(mobileDataServicesCharge.getChargingService(),
					bill.getBillingPeriod());
			report.setMobileDataServiceDays(Long.toString(NoOfChargingDays));
		}
		IChargable mobileRoamingCharge = getChargeByDesc(charges, Charges.mobileRoamingSubscriptionCharge.toString());
		if (mobileRoamingCharge != null) {
			report.setMobileRoaming(mobileRoamingCharge.charge(bill));
			long NoOfChargingDays = PerDay.getNoOfChargingDays(mobileRoamingCharge.getChargingService(),
					bill.getBillingPeriod());
			report.setMobileRoamingDays(Long.toString(NoOfChargingDays));
		}
		IChargable mobileLineCharge = getChargeByDesc(charges, Charges.MobileLineSubscriptionCharge.toString());
		if (mobileLineCharge != null) {
			report.setMobileLine(mobileLineCharge.charge(bill));
			long NoOfChargingDays = PerDay.getNoOfChargingDays(mobileLineCharge.getChargingService(),
					bill.getBillingPeriod());
			report.setMobileLineDays(Long.toString(NoOfChargingDays));
		}
		IChargable mobileLocalCallCharge = getChargeByDesc(charges, Charges.MobileLocalUsageCharge.toString());
		if (mobileLocalCallCharge != null) {
			report.setMobileLocalCalls(mobileLocalCallCharge.charge(bill));
		}
		IChargable mobileIDDCallCharge = getChargeByDesc(charges, Charges.MobileIDDUsageCharge.toString());
		if (mobileIDDCallCharge != null) {
			report.setMobileIDDCalls(mobileIDDCallCharge.charge(bill));
		}
		IChargable mobileRoamingCallCharge = getChargeByDesc(charges, Charges.mobileRoamingUsageCharge.toString());
		if (mobileRoamingCallCharge != null) {
			report.setMobileRoamingCalls(mobileRoamingCallCharge.charge(bill));
		}

		return report;
	}

	private static BillReport mapDigitalLineCharges(List<IChargable> charges, Bill bill, BillReport report) {
		IChargable digitalCallTransterCharge = getChargeByDesc(charges,
				Charges.DigitalCallTransferSubscriptionCharge.toString());
		if (digitalCallTransterCharge != null) {
			report.setDigitalCallTranster(digitalCallTransterCharge.charge(bill));
			long NoOfChargingDays = PerDay.getNoOfChargingDays(digitalCallTransterCharge.getChargingService(),
					bill.getBillingPeriod());
			report.setDigitalCallTransterDays(Long.toString(NoOfChargingDays));
		}

		IChargable digitalIDDUsageCharge = getChargeByDesc(charges, Charges.DigitalIDDUsageCharge.toString());
		if (digitalIDDUsageCharge != null) {
			report.setDigitalIDDCalls(digitalIDDUsageCharge.charge(bill));
		}

		IChargable digitalLineSubscriptionCharge = getChargeByDesc(charges,
				Charges.DigitalLineSubscriptionCharge.toString());
		if (digitalLineSubscriptionCharge != null) {
			report.setDigitalLine(digitalLineSubscriptionCharge.charge(bill));
			long NoOfChargingDays = PerDay.getNoOfChargingDays(digitalLineSubscriptionCharge.getChargingService(),
					bill.getBillingPeriod());
			report.setDigitalLineDays(Long.toString(NoOfChargingDays));
		}

		IChargable digitalLocalUsageCharge = getChargeByDesc(charges, Charges.DigitalLocalUsageCharge.toString());
		if (digitalLocalUsageCharge != null) {
			report.setDigitalLocalCalls(digitalLocalUsageCharge.charge(bill));
		}

		return report;
	}

	private static BillReport mapCableCharges(List<IChargable> charges, Bill bill, BillReport report) {
		report.setCableTVSubscriptionCharges(getChargeByDesc(charges, Charges.CableTVSubscriptionCharge.toString()) != null ? getChargeByDesc(
				charges, Charges.CableTVSubscriptionCharge.toString()).charge(bill)
				: 0);

		report.setCableTVUsageCharges(getTotalAdditionCableCharges(charges, bill));
		report.setAdditionalCableCount(getTotalAdditionCableCount(charges));

		report.setAdditionalCableCountLable(report.getAdditionalCableCountLable() +  getTotalAdditionCableCountLable(getAdditionCableCharges(charges),
				bill.getBillingPeriod()));
		return report;
	}

	private static double getTotalAdditionCableCharges(List<IChargable> charges, Bill chargingBill) {
		double totalCharge = 0.00;
		for (IChargable charge : charges) {
			if (charge.getDescription().equals(Charges.AdditionalChSubscriptionCharge.toString()))
				totalCharge = totalCharge + charge.charge(chargingBill);

		}
		return totalCharge;

	}

	protected static List<IChargable> getAdditionCableCharges(List<IChargable> charges) {
		List<IChargable> additionCableCharges = new ArrayList<IChargable>();
		for (IChargable charge : charges) {
			if (charge.getDescription().equals(Charges.AdditionalChSubscriptionCharge.toString()))
				additionCableCharges.add(charge);

		}
		return additionCableCharges;

	}

	private static int getTotalAdditionCableCount(List<IChargable> charges) {
		int count = 0;
		for (IChargable charge : charges) {
			if (charge.getDescription().equals(Charges.AdditionalChSubscriptionCharge.toString()))
				count++;

		}
		return count;

	}

	protected static String getTotalAdditionCableCountLable(List<IChargable> serviceCharges, BillingPeriod period) {
		StringBuilder htmlBuilder = new StringBuilder();

		Map<ISubscribable, List<IChargable>> chargesByServices = groupChargesByServicePeriod(serviceCharges);
		if (chargesByServices.size() < 1) {
			return "";
		}
		Iterator it = chargesByServices.entrySet().iterator();
		htmlBuilder.append("<p>");
		while (it.hasNext()) {
			Map.Entry pairs = (Map.Entry) it.next();

			htmlBuilder.append(((ISubscribable) pairs.getKey()).getCharges().get(0).getRate().getAmount() + " x ");
			htmlBuilder.append(((List<IChargable>) pairs.getValue()).size() + " ch(s)");
			htmlBuilder.append(" (" + PerDay.getNoOfChargingDays(((ISubscribable) pairs.getKey()), period) + " days)");

			htmlBuilder.append("</br>");

		}
		htmlBuilder.append("</p>");
		return htmlBuilder.toString();
	}

	protected static Map<ISubscribable, List<IChargable>> groupChargesByServicePeriod(List<IChargable> charges) {

		Map<ISubscribable, List<IChargable>> chargesByServices = new HashMap<ISubscribable, List<IChargable>>();
		ISubscribable index = null;
		for (IChargable charge : charges) {
			if (index == null || !index.getDateCommenced().equals(charge.getChargingService().getDateCommenced())) {
				index = charge.getChargingService();
				chargesByServices.put(index, groupChargesByServicePeriod(index, charges));
			}
		}
		return chargesByServices;
	}

	protected static List<IChargable> groupChargesByServicePeriod(ISubscribable service, List<IChargable> charges) {

		List<IChargable> groupedCharges = new ArrayList<IChargable>();
		for (IChargable charge : charges) {
			if (service.getDateCommenced().equals(charge.getChargingService().getDateCommenced()))
				groupedCharges.add(charge);
		}
		return groupedCharges;

	}

	private static IChargable getChargeByDesc(List<IChargable> charges, String desc) {

		for (IChargable charge : charges) {
			if (charge.getDescription().equals(desc))

				return charge;

		}
		return null;
	}

	public static boolean isServiceRequiredToBeCharged(ISubscribable service, BillingPeriod period) {

		Date serviceCommercedDate, serviceTerminatedDate;

		serviceCommercedDate = DateHelper.getDate(service.getDateCommenced(), "dd-MMM-yyyy");
		serviceTerminatedDate = (service.getDateTerminated() != null && service.getDateTerminated() != "" ? DateHelper
				.getDate(service.getDateTerminated(), "dd-MMM-yyyy") : null);

		return ((serviceTerminatedDate == null || serviceTerminatedDate.after(period.getStartDate())) && serviceCommercedDate
				.before(period.getEndDate()));

	}

	public static boolean isSubscriptionRequiredToBeCharged(ISubscription subscription, BillingPeriod period) {

		Date serviceCommercedDate, serviceTerminatedDate;

		serviceCommercedDate = DateHelper.getDate(subscription.getDateCommenced(), "dd-MMM-yyyy");
		serviceTerminatedDate = (subscription.getDateTerminated() != null && subscription.getDateTerminated() != "" ? DateHelper
				.getDate(subscription.getDateTerminated(), "dd-MMM-yyyy") : null);

		return ((serviceTerminatedDate == null || serviceTerminatedDate.after(period.getStartDate())) && serviceCommercedDate
				.before(period.getEndDate()));

	}

	public static BillMonth getBillMonth(String month) {
		if ("01".equals(month))
			return BillMonth.Jan;
		else if ("02".equals(month))
			return BillMonth.Feb;
		else if ("03".equals(month))
			return BillMonth.March;
		else if ("04".equals(month))
			return BillMonth.April;
		else if ("05".equals(month))
			return BillMonth.May;
		else if ("06".equals(month))
			return BillMonth.June;
		else if ("07".equals(month))
			return BillMonth.July;
		else if ("08".equals(month))
			return BillMonth.August;
		else if ("09".equals(month))
			return BillMonth.Septemper;
		else if ("10".equals(month))
			return BillMonth.Octorber;
		else if ("11".equals(month))
			return BillMonth.November;
		else if ("12".equals(month))
			return BillMonth.December;
		return null;
	}
}
